package main

import (
	"crypto/md5"
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

import (
	"io/ioutil"
	"log"
	"sort"
)

func init() {
	flag.BoolVar(&useSmartHash, "useSmartHash", true, "使用文件20份部分读取的内容来计算文件MD5,更快,节约读硬盘时间")
	flag.BoolVar(&sameFileName, "sameFilename", true, "使用相同文件名来进行重复文件判定,设置false则不把<文件名>加入唯一哈希判定")
	flag.BoolVar(&sameModTime, "sameModTime", true, "使用相同的文件修改日期来进行重复文件判定,改为false则会不把<修改日期>加入唯一哈希判定")
	flag.BoolVar(&keepOldFile, "keepOldFile", true, "保留更早修改日期的文件")
	flag.StringVar(&minFileSizeStr, "minFileSize", "1KB", "低于此文件大小的文件将不会被加入去重;默认不限制")
	flag.StringVar(&maxFileSizeStr, "maxFileSize", "1000KB", "高于此文件大小的文件将不会被加入去重;默认不限制,<=0为不限制")
	flag.BoolVar(&debug, "debug", debug, "开启调试日志")
	flag.BoolVar(&dryRun, "dryRun", dryRun, "不进行文件删除操作. 只打印.")

	var err error
	MinFileSize, err = ConvertToByte(minFileSizeStr)
	checkLogErr(err)
	MaxFileSize, err = ConvertToByte(maxFileSizeStr)
	checkLogErr(err)
	if !debug {
		Debug.SetOutput(NullWriter())
	}
}

func checkLogErr(err error) {
	if err != nil {
		Warn.Println("error:", err)
	}
}

// 命令行参数
var (
	debug                 = false
	dryRun                = true
	useSmartHash          = true
	sameFileName          = true
	sameModTime           = true
	keepOldFile           = true
	minFileSizeStr string = "50KB" //最小文件大小,单位:字节
	maxFileSizeStr string = "0"    //最大文件大小,单位:字节

	MinFileSize int64 = 0 //最小文件大小,单位:字节
	MaxFileSize int64 = 0 //最大文件大小,单位:字节
)

// smartHash 智能文件读取,只读取文件20份部分内容来计算MD5
const minChunkSize = 500 * 1024  //切分文件后,每一份最小读取的字节数 例如:500KB一段,20个就是10MB
const MinSize = 20 * 1024 * 1024 //智能文件读取最小文件字节 例如:20MB以上才开启智能文件读取 加快hash计算

// 文件信息
type FileInfo struct {
	Name        string
	Size        int64
	Path        string
	ModTime     time.Time
	CreateTime  time.Time //go 无法直接读取创建时间,需要使用syscall.Win32FileAttributeData
	ContentMD5  string
	IsDuplicate bool
}

// loadFileInfo 函数从给定的目录路径中加载文件信息，并返回一个FileInfo数组和错误信息
// dirPath: 需要加载文件信息的目录路径
// 返回值:
//   - []*FileInfo: 文件信息数组，每个元素包含文件名、大小、路径、修改时间、创建时间、文件内容、文件内容MD5和是否重复等信息
//   - error: 错误信息，如果加载成功则为nil
func loadFileInfo(dirPath string) ([]*FileInfo, error) {
	var fileInfos []*FileInfo

	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if !info.IsDir() {
			Debug.Println("  walk file:", path)
			if MinFileSize > 0 && info.Size() <= MinFileSize {
				Debug.Println("    cause MinFileSize, skip file:", path)
				return nil
			}
			if MaxFileSize > 0 && info.Size() >= MaxFileSize {
				Debug.Println("    cause MaxFileSize, skip file:", path)
				return nil
			}
			//// 尝试获取文件的创建时间（注意：不是所有操作系统都支持）
			//creationTime := info.Sys().(*syscall.Win32FileAttributeData).CreationTime
			//if creationTime.Nanoseconds() == 0 { //IsZero()
			//	Info.Println("Creation Time is not supported on this platform.")
			//} else {
			//	Info.Printf("Creation Time: %s\n", creationTime.Format(time.RFC3339))
			//}
			//// 尝试获取文件的访问时间（注意：不是所有操作系统都支持）
			//accessTime := info.Sys().(*syscall.Win32FileAttributeData).LastAccessTime
			//if accessTime.Nanoseconds() == 0 { //IsZero()
			//	Info.Println("Access Time is not supported on this platform.")
			//} else {
			//	Info.Printf("Access Time: %s\n", accessTime.Format(time.RFC3339))
			//}
			fileInfo := &FileInfo{
				Name:        info.Name(),
				Size:        info.Size(),
				Path:        path,
				ModTime:     info.ModTime(),
				CreateTime:  info.ModTime(),
				ContentMD5:  "",
				IsDuplicate: false,
			}

			////读取文件内容,用于计算MD5哈希值,后续用于重复文件判断; TODO 可以延后到确定有大小相同的文件后,再读取文件内容
			//content, err2 := getSmartFileContent(path, info, fileInfo)
			//if err2 != nil {
			//	return err2
			//}
			//fileInfo.ContentMD5 += fmt.Sprint(info.Size(), "|", fmt.Sprintf("%x", md5.Sum(content)))
			//

			fileInfos = append(fileInfos, fileInfo)
		} else {
			Debug.Println("  walk Dir:", path)
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	return fileInfos, nil
}

// findDuplicateFiles 函数接收一个FileInfo类型的切片作为参数，返回一个map[string][]*FileInfo类型的值
func findDuplicateFiles(fileInfos []*FileInfo) map[string][]*FileInfo {
	sameSizeFileMap := make(map[int64][]*FileInfo) //key1=size,key2=相同大小的文件列表
	duplicates := make(map[string][]*FileInfo)     //key1=MD5哈希,key2=相同hash的重复文件列表
	Debug.Println("  total files:", len(fileInfos), ", start check dump...")

	for _, fileInfo := range fileInfos {
		sameSizeFileMap[fileInfo.Size] = append(sameSizeFileMap[fileInfo.Size], fileInfo)
	}
	var AloneSameSizeFile int
	var RepeatSameSizeCount int
	//构造 duplicates
	for size, sameSizeFiles := range sameSizeFileMap {
		if len(sameSizeFiles) == 1 {
			fileInfo := sameSizeFiles[0]
			fileInfo.ContentMD5 = "HashOneSize:" + fmt.Sprint(size)
			//duplicates[fileInfo.ContentMD5] = sameSizeFiles
			duplicates[fileInfo.ContentMD5] = append(duplicates[fileInfo.ContentMD5], sameSizeFiles...)
			AloneSameSizeFile++
			continue
		}
		RepeatSameSizeCount++

		//文件大小 必须是 完全一致的 才有可能重复
		//多个相同大小的文件再读取文件并计算MD5值 (采用 聪明方法读取文件的内容,并计算MD5哈希值)
		for _, fileInfo := range sameSizeFiles {
			//读取文件内容,用于计算MD5哈希值,后续用于重复文件判断; TODO 可以延后到确定有大小相同的文件后,再读取文件内容
			content, err2 := getSmartFileContent(fileInfo.Path, fileInfo)
			if err2 != nil {
				//return err2
				Error.Println(fileInfo.Path, err2)
				continue
			}
			fileInfo.ContentMD5 = ""
			//文件名 完全一致 默认为true,可以考虑关闭掉
			if sameFileName {
				fileInfo.ContentMD5 += fileInfo.Name + "|"
			}
			//修改日期 完全一致  默认为true,可以考虑关闭掉
			if sameModTime {
				fileInfo.ContentMD5 += fileInfo.ModTime.Format(time.DateTime) + "|"
			}
			fileInfo.ContentMD5 += fmt.Sprint(fmt.Sprintf("%x", md5.Sum(content)))
			duplicates[fileInfo.ContentMD5] = append(duplicates[fileInfo.ContentMD5], fileInfo)
		}
	}
	Warn.Println("  total AloneSameSizeFile:", AloneSameSizeFile, ", total RepeatSameSizeGroupNum:", RepeatSameSizeCount, "[not final num]")
	for MD5Str, fileInfo := range duplicates {
		_ = MD5Str
		// 如果超过一个文件，则说明有重复
		if len(fileInfo) > 1 {
			for i := 1; i < len(fileInfo); i++ {
				fileInfo[i].IsDuplicate = true
			}
		}
	}

	//for _, fileInfo := range fileInfos {
	//	duplicates[fileInfo.ContentMD5] = append(duplicates[fileInfo.ContentMD5], fileInfo)
	//}
	//for MD5, dupFiles := range duplicates {
	//	_ = MD5
	//	// 如果超过一个文件，则说明有重复
	//	if len(dupFiles) > 1 {
	//		for i := 1; i < len(dupFiles); i++ {
	//			dupFiles[i].IsDuplicate = true
	//		}
	//	}
	//}
	return duplicates
}

func removeDuplicateFiles(fileInfos []*FileInfo) {
	duplicates := findDuplicateFiles(fileInfos)
	noDumpFileCount := 0
	dumpFileCount := 0
	dumpUniqFileCount := 0
	failedErr := 0
	for md5str, dupFiles := range duplicates {
		_ = md5str
		if len(dupFiles) > 1 {
			Info.Println("  Find>>> duplicate files found for ", dupFiles[0].Name, dupFiles[0].Size, md5str)
			sort.Slice(dupFiles, func(i, j int) bool {
				a := dupFiles[i].ModTime.UnixMilli()
				b := dupFiles[j].ModTime.UnixMilli()
				//相同日期 就保留原本顺序
				if a == b {
					return false
				}
				// 保留更小的日期(日期更早);
				if keepOldFile {
					return a < b
				} else {
					return a > b
				}
			})
			// 保留最老的文件
			Info.Printf("    keep Only One duplicate files for %s,modTime=%v\n", dupFiles[0].Path, dupFiles[0].ModTime.Format(time.DateTime))
			for i := 1; i < len(dupFiles); i++ {
				var err error
				if dryRun {
					err = nil
				} else {
					err = os.Remove(dupFiles[i].Path)
				}
				if err != nil {
					Info.Printf("      Error removing file %s: %v\n", dupFiles[i].Path, err)
					failedErr++
				} else {
					Info.Printf("      Removed duplicate file:%s,modTime=%v\n", dupFiles[i].Path, dupFiles[i].ModTime.Format(time.DateTime))
					dumpFileCount++
				}
			}
			dumpUniqFileCount++
		} else {
			//Info.Printf("No duplicate files found for %s\n", dupFiles[0].Path)
			noDumpFileCount++
		}
	}
	Info.Printf("Total NoDuplicate files: %d\n", noDumpFileCount)
	Info.Printf("Total dumpFileCount:%v  uniq=%v\n", dumpFileCount, dumpUniqFileCount)
	Info.Printf("Total failedErr:%v  \n", failedErr)
}

var dirs []string // Replace with actual directory paths

func main() {

	flag.Parse()
	//flag.Usage = func() {}

	// 通过参数来获取要处理的目录
	if len(os.Args) > 1 {
		dirs = os.Args[1:]
	}
	if len(dirs) == 0 {
		log.Fatal("No directory paths provided")
	}
	//打印命令行参数
	for _, dir := range dirs {
		if strings.HasPrefix(dir, "-") {
			continue
		}
		Info.Printf("Processing directory: %s\n", dir)
	}
	Info.Println("是否使用更快的哈希算法: -useSmartHash=", useSmartHash)
	Info.Println("是否使用<修改日期>去重: -sameModTime=", sameModTime)
	if !sameModTime {
		Info.Println("是否保留修改日期较新的文件: keepOldFile=", keepOldFile)
	}
	Info.Println("是否使用<文件名>去重:-sameFileName=", sameFileName)
	Info.Println("去重最小文件大小: -MinFileSize=", MinFileSize)
	Info.Println("去重最大文件大小: -MaxFileSize=", MaxFileSize)
	Info.Println("logger: -debug=", debug)
	Info.Println("option: -dryRun=", dryRun)

	Info.Println("1.========读取目录的所有文件并计算hash")
	var allFileInfos []*FileInfo
	for _, dir := range dirs {
		if strings.HasPrefix(dir, "-") {
			continue
		}
		fileInfos, err := loadFileInfo(dir)
		if err != nil {
			Info.Printf("Error loading files from %s: %v\n", dir, err)
			continue
		}
		allFileInfos = append(allFileInfos, fileInfos...)
	}

	Info.Println("2.========检测是否有重复文件 并移除")
	removeDuplicateFiles(allFileInfos)
	Info.Println("3.========移除所有的空目录")
	removeEmptyDirs(dirs)

	// 调用pause命令，让用户可以查看输出结果
	cmd := exec.Command("pause")
	cmd.Stdin = os.Stdin
	cmd.Run()
	// 让用户按任意键退出,如果有空格键,则立刻退出
	fmt.Println("Press any key to exit...")
	var input string
	//fmt.Scanln(&input)
	//fmt.Println("Bye!")
	//读取一个键盘按键并存储到一个byte变量中
	fmt.Fscanf(os.Stdin, "%s ", &input)
	fmt.Println("Bye!")

}
func removeEmptyDirs(dirs []string) {
	for _, dir := range dirs {
		if strings.HasPrefix(dir, "-") {
			continue
		}
		err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return err
			}
			if info.IsDir() {
				entries, err := ioutil.ReadDir(path)
				if err != nil {
					Warn.Println("ReadDir failed:", path, err)
				}
				if len(entries) == 0 {
					if dryRun {
						err = nil
					} else {
						err := os.Remove(path)
						if err != nil {
							Info.Printf("Removed empty directory: %s Error: %v\n", path, err)
							return nil
						}
					}
					Info.Printf("Removed empty directory: %s\n", path)
				}
			}
			return nil
		})
		if err != nil {
			Info.Printf("Error removing empty directories from %s: %v\n", dir, err)
		}
	}
}

//
//func removeEmptyDirectories(dirPath string) error {
//	err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
//		if err != nil {
//			return err
//		}
//		if info.IsDir() {
//			isEmpty, _ := isDirEmpty(path)
//			if isEmpty {
//				err := os.Remove(path)
//				if err != nil {
//					Info.Printf("Error removing directory %s: %v\n", path, err)
//				} else {
//					Info.Printf("Removed empty directory: %s\n", path)
//				}
//			}
//		}
//		return nil
//	})
//	if err != nil {
//		return err
//	}
//
//	return nil
//}
//
//func isDirEmpty(name string) (bool, error) {
//	f, err := os.Open(name)
//	if err != nil {
//		return false, err
//	}
//	defer f.Close()
//
//	_, err = f.Readdirnames(1)
//	if err == io.EOF {
//		return true, nil
//	}
//	return false, err
//}

//
//type FileInfo struct {
//	FileName   string
//	FileSize   int64
//	FilePath   string
//	ModTime    string
//	CreateTime string
//}
//
//type DumpRemover struct {
//	//TODO: Implement the DumpRemover struct
//}
//
//// 加载目录下所有文件,并记录 文件名 文件大小 文件路径(用于后续读取内容和计算MD5) 文件修改时间,创建时间 等文件信息
//func (d *DumpRemover) loadDir(dir string) {
//	dirPath := "/path/to/directory"
//	files, err := ioutil.ReadDir(dirPath)
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	for _, file := range files {
//		fileInfo := &FileInfo{
//			FileName:   file.Name(),
//			FileSize:   file.Size(),
//			FilePath:   dirPath + string(os.PathSeparator) + file.Name(),
//			ModTime:    file.ModTime().String(),
//			CreateTime: file.Sys().(*syscall.Stat_t).Ctimespec.String(),
//		}
//		Info.Println(fileInfo)
//	}
//}
